/* Turska Framework/UI library (Turs2)
 * Copyright 2003-2010 Jetro Lauha
 * All rights reserved.
 * Web: http://iki.fi/jetro/turska/
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of EITHER:
 *   (1) The GNU Lesser General Public License as published by the Free
 *       Software Foundation; either version 2.1 of the License, or (at
 *       your option) any later version. The text of the GNU Lesser
 *       General Public License is included with this library in the
 *       file LICENSE-LGPL.txt.
 *   (2) The BSD-style license that is included with this library in
 *       the file LICENSE-BSD.txt.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the files
 * LICENSE-LGPL.txt and LICENSE-BSD.txt for more details.
 *
 * $Id: T2GraphicsOpenGL.cpp 28 2012-07-28 17:03:06Z trzntaat $
 * $Revision: 28 $
 */

#include <string.h>

#include "Turs2.h"


#if defined(T2_USE_OPENGL) || defined(T2_USE_OPENGL_ES)

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif // _WIN32

#include <assert.h>

#ifdef T2_USE_OPENGL
#ifdef __APPLE__
    #include <OpenGL/gl.h>
    #include <OpenGL/glu.h>
#else
    #include "GL/gl.h"
    #include "GL/glu.h"
#endif
#else // T2_USE_OPENGL_ES:
    #ifdef T2_OPENGL_ES_IPHONE
        #include <OpenGLES/ES1/gl.h>
        #include <OpenGLES/ES1/glext.h>
    #else // T2_OPENGL_ES_GENERIC:
        #include <GLES/gl.h>
        #include <GLES/glext.h>
    #endif
#endif


#include "T2GraphicsOpenGL.h"

#include "stb_image.h"


// req. OpenGL version 1.2
#ifndef GL_CLAMP_TO_EDGE
#define GL_CLAMP_TO_EDGE                  0x812F
#endif


namespace turska
{


static inline UI32 sNextPowerOfTwo(UI32 input)
{
     --input;
     input |= input >> 16;
     input |= input >> 8;
     input |= input >> 4;
     input |= input >> 2;
     input |= input >> 1;
     return input + 1;
}


void GraphicsOpenGL::beginRender()
{
#ifdef T2_USE_OPENGL
    // TODO: Support for optionally saving/restoring existing state w/OpenGL ES?
    // It is currently omitted since querying current state with glGet* is supposedly slow.
    glPushAttrib(GL_COLOR_BUFFER_BIT);
    glPushAttrib(GL_ENABLE_BIT);
#endif // T2_USE_OPENGL

    glDisable(GL_DEPTH_TEST);
    glDisable(GL_CULL_FACE);
    glEnable(GL_TEXTURE_2D);

    glEnable(GL_BLEND);
    //glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glViewport(0, 0, mWindowWidth, mWindowHeight);

    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();

#ifdef T2_USE_OPENGL
    glOrtho(0, (GLdouble)mWindowWidth, (GLdouble)mWindowHeight, 0, 0, 1);
#else
    glOrthof(0, (GLfloat)mWindowWidth, (GLfloat)mWindowHeight, 0, 0, 1);
#endif

    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    //glLoadIdentity();
    glLoadMatrixf(mInitialModelViewMatrix);

    // This translate is useful only if both target coords & sampling use
    // 1:1 integer coordinates, possibly also with nearest sampling mode.
    // Our aim is to support sub-pixel precision, so this is omitted.
    // TODO - supply a hint for beginRender if scaling is used or not?
    //glTranslatef(0.375f, 0.375f, 0);

    //glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
#ifdef T2_USE_OPENGL
    const GLubyte color[4] = { 255, 255, 255, 255 };
    glColor4ubv(color);
#else
    glColor4ub(255, 255, 255, 255);
#endif

    glDisableClientState(GL_NORMAL_ARRAY);
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    glEnableClientState(GL_COLOR_ARRAY);

    mCurrentBatchVertexCount = 0;
    mCurrentBatchTriangleCount = 0;
    mCurrentBatchTextureGLId = 0;
    mCurrentBatchDrawMode = IDM_NORMAL;
    mBatchIndices.clear();
    mBatchPositions.clear();
    mBatchTexCoords.clear();
    mBatchColors.clear();

    mStatBatchCount = 0;
    mStatTriangleCount = 0;
}


void GraphicsOpenGL::endRender()
{
    flush();

    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
    glDisableClientState(GL_COLOR_ARRAY);

    glMatrixMode(GL_MODELVIEW);
    glPopMatrix();

    glMatrixMode(GL_PROJECTION);
    glPopMatrix();

#ifdef T2_USE_OPENGL
    glPopAttrib(); // GL_ENABLE_BIT
    glPopAttrib(); // GL_COLOR_BUFFER_BIT
#endif // T2_USE_OPENGL
}


Image * GraphicsOpenGL::loadImage(const C8 *fileName, UI32 flags)
{
    const int req_comp = 4;
    int width, height, comp;
    UI8 *data = stbi_load(fileName, &width, &height, &comp, req_comp);
    if (data == 0)
        return 0;
    Image *result = createImage(data, width, height, flags);
    stbi_image_free(data);
    return result;
}


Image * GraphicsOpenGL::loadImage(const UI8 *buffer, I32 length, UI32 flags)
{
    if (buffer == 0 || length <= 0)
        return 0;
    const int req_comp = 4;
    int width, height, comp;
    UI8 *data = stbi_load_from_memory(buffer, length, &width, &height, &comp, req_comp);
    Image *result = createImage(data, width, height, flags);
    stbi_image_free(data);
    return result;
}


Image * GraphicsOpenGL::createImage(const UI8 *buffer, I32 width, I32 height,
                                    UI32 flags, PixelRectangle *sourceRect,
                                    I32 components)
{
    if (buffer == 0 || width <= 0 || height <= 0)
        return 0;

    if (flags == G_USE_DEFAULT_FLAGS)
        flags = mDefaultFlags;

    bool success;
    UI32 texture;
    F32 uv[4];

    success = createTexture(texture, uv, buffer, width, height, flags, sourceRect, components);
    if (!success)
        return 0;

    if (sourceRect == 0)
        return new ImageOpenGL(this, width, height, flags, texture, uv, true);
    else
        return new ImageOpenGL(this, sourceRect->width, sourceRect->height, flags, texture, uv, true);
}


static void sSimpleFixAlphaChannel(UI8 *buffer, I32 width, I32 height, const PixelRectangle &area, int components = 4)
{
    assert(components == 4);

    if (width < 2 || height < 2)
        return;

    I32 x, y;
    const I32 w = width, h = height;
    const I32 rOffs = 0;
    const I32 gOffs = 1;
    const I32 bOffs = 2;
    const I32 alphaOffs = 3;

    I32 co = 0;
    for (y = 0; y < h; ++y)
    {
        for (x = 0; x < w; ++x)
        {
            if (buffer[co + alphaOffs] == 0)
            {
                // iterate through 3x3 window around pixel
                I32 left = x - 1, right = x + 1, top = y - 1, bottom = y + 1;
                if (left < 0) left = 0;
                if (right >= w) right = w - 1;
                if (top < 0) top = 0;
                if (bottom >= h) bottom = h - 1;
                I32 x2, y2, colors = 0, co2 = (top * w + left) * components;
                I32 red = 0, green = 0, blue = 0;
                for (y2 = top; y2 <= bottom; ++y2)
                {
                    for (x2 = left; x2 <= right; ++x2)
                    {
                        if (buffer[co2 + alphaOffs])
                        {
                            red += buffer[co2 + rOffs];
                            green += buffer[co2 + gOffs];
                            blue += buffer[co2 + bOffs];
                            ++colors;
                        }
                        co2 += components;
                    }
                    co2 += (w - (right - left + 1)) * components;
                }
                if (colors > 0)
                {
                    buffer[co + rOffs] = red / colors;
                    buffer[co + gOffs] = green / colors;
                    buffer[co + bOffs] = blue / colors;
                }
            }
            co += components;
        }
    }

    // if rect is smaller than texture, copy rightmost/bottom col/row (and pixel at w,h)
    // as is from the inner one so that linear sampling works like clamp_to_edge
    if (area.width < w)
    {
        assert(components == 4);
        UI32 *pixels = (UI32 *)buffer;
        for (y = 0; y < area.height; ++y)
            pixels[y * w + area.width] = pixels[y * w + area.width - 1];
    }
    if (area.height < h)
    {
        assert(components == 4);
        UI32 *pixels = (UI32 *)buffer;
        for (x = 0; x < area.width; ++x)
            pixels[area.height * w + x] = pixels[(area.height - 1) * w + x];
    }
    if (area.width < w && area.height < h)
    {
        assert(components == 4);
        UI32 *pixels = (UI32 *)buffer;
        pixels[area.height * w + area.width] = pixels[(area.height - 1) * w + area.width - 1];
    }

    /*
    // Test code to see all pixels with full opacity
    for (y = 0; y < h; ++y)
    {
        for (x = 0; x < w; ++x)
        {
            buffer[(y * w + x) * components + alphaOffs] = 0xff;
        }
    }
    */
}


bool GraphicsOpenGL::createTexture(UI32 &oTexture, F32 *oUV, const UI8 *buffer,
                                   I32 width, I32 height, UI32 flags,
                                   PixelRectangle *sourceRect, I32 components)
{
    assert(components == 4);
    assert(oUV != 0);

    I32 w, h;
    PixelRectangle area, srcr;
    I32 srcw, srch;

    if (sourceRect != 0)
    {
        srcw = sourceRect->width;
        srch = sourceRect->height;
    }
    else
    {
        srcw = width;
        srch = height;
    }

    // Use the surface width and height expanded to powers of 2.
    w = sNextPowerOfTwo(srcw);
    h = sNextPowerOfTwo(srch);
    oUV[0] = 0;                     // Min X
    oUV[1] = 0;                     // Min Y
    oUV[2] = ((GLfloat)srcw) / w;   // Max X
    oUV[3] = ((GLfloat)srch) / h;   // Max Y

    UI8 *image = new UI8[w * h * components];
    if (image == 0)
        return false;
    memset(image, 0, w * h * components);

    // Copy the surface into the GL texture image.
    area.x = 0;
    area.y = 0;
    area.width = srcw;
    area.height = srch;
    if (sourceRect != 0)
        srcr = *sourceRect;
    else
        srcr = area;
    int dstRowOffs = 0, srcRowOffs = (srcr.y * width + srcr.x) * components;
    for (int y = 0; y < h; ++y)
    {
        memcpy(image + dstRowOffs, buffer + srcRowOffs, area.width * components);
        dstRowOffs += w * components;
        srcRowOffs += width * components;
    }

    // TODO: Should this "fix" be optional?
    sSimpleFixAlphaChannel(image, w, h, area, components);

    // Create an OpenGL texture from the image.
    glGenTextures(1, (GLuint *)&oTexture);
    glBindTexture(GL_TEXTURE_2D, oTexture);

    if (flags & IF_FILTER_LINEAR_MIPMAP)
        flags |= IF_USE_MIPMAPS;

    if (flags & IF_FILTER_LINEAR)
    {
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        if (flags & IF_USE_MIPMAPS)
        {
            if (flags & IF_FILTER_LINEAR_MIPMAP)
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
            else
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
        }
        else
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    }
    else
    {
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        if (flags & IF_USE_MIPMAPS)
        {
            if (flags & IF_FILTER_LINEAR_MIPMAP)
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
            else
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
        }
        else
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    }

    if (flags & IF_WRAP_REPEAT)
    {
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    }
    else
    {
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    }

    // for some tests - bright green border color
    //GLfloat col[4] = { 0, 1, 0, 1 };
    //glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, col);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);

    if (flags & IF_USE_MIPMAPS)
    {
        #ifdef T2_USE_OPENGL
        gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGBA, w, h, GL_RGBA, GL_UNSIGNED_BYTE, image);
        #else // or T2_USE_OPENGL_ES:
        #ifdef GL_OES_framebuffer_object
        glGenerateMipmapOES(GL_TEXTURE_2D);
        #endif // GL_OES_framebuffer_object
        #endif // T2_USE_OPENGL_ES
    }

    delete[] image;

    return true;
}


void GraphicsOpenGL::prepareBatch(UI32 textureGLId, turska::IMAGE_DRAW_MODE drawMode, bool forceFlush)
{
    if (textureGLId != mCurrentBatchTextureGLId ||
        drawMode != mCurrentBatchDrawMode ||
        mCurrentBatchVertexCount >= (65536 - 4) ||
        forceFlush)
    {
        flush();
        mCurrentBatchTextureGLId = textureGLId;
        mCurrentBatchDrawMode = drawMode;
    }
}


void GraphicsOpenGL::batchImageDraw(UI32 textureGLId, turska::IMAGE_DRAW_MODE drawMode, const F32 *positions, const F32 *texCoords, const UI8 *colors)
{
    prepareBatch(textureGLId, drawMode, false);

    // Stuff has been unrolled here with belief that it might be faster,
    // but no benchmarks have been done with the likely hardware.
    const UI16 newVtxIdx = (UI16)mCurrentBatchVertexCount;
    //static const u16 refIndices[6] = { 0, 1, 2,  3, 2, 1 };
    mBatchIndices.add(newVtxIdx);     // tri1 v1
    mBatchIndices.add(newVtxIdx + 1); // tri1 v2
    mBatchIndices.add(newVtxIdx + 2); // tri1 v3
    mBatchIndices.add(newVtxIdx + 3); // tri2 v1
    mBatchIndices.add(newVtxIdx + 2); // tri2 v2
    mBatchIndices.add(newVtxIdx + 1); // tri2 v3
    mBatchPositions.add(*positions++); // x1
    mBatchPositions.add(*positions++); // y1
    mBatchPositions.add(*positions++); // x2
    mBatchPositions.add(*positions++); // y2
    mBatchPositions.add(*positions++); // x3
    mBatchPositions.add(*positions++); // y3
    mBatchPositions.add(*positions++); // x4
    mBatchPositions.add(*positions);   // y4
    mBatchTexCoords.add(*texCoords++); // u1
    mBatchTexCoords.add(*texCoords++); // v1
    mBatchTexCoords.add(*texCoords++); // u2
    mBatchTexCoords.add(*texCoords++); // v2
    mBatchTexCoords.add(*texCoords++); // u3
    mBatchTexCoords.add(*texCoords++); // v3
    mBatchTexCoords.add(*texCoords++); // u4
    mBatchTexCoords.add(*texCoords);   // v4
    mBatchColors.add(*colors++); // r1
    mBatchColors.add(*colors++); // g1
    mBatchColors.add(*colors++); // b1
    mBatchColors.add(*colors++); // a1
    mBatchColors.add(*colors++); // r2
    mBatchColors.add(*colors++); // g2
    mBatchColors.add(*colors++); // b2
    mBatchColors.add(*colors++); // a2
    mBatchColors.add(*colors++); // r3
    mBatchColors.add(*colors++); // g3
    mBatchColors.add(*colors++); // b3
    mBatchColors.add(*colors++); // a3
    mBatchColors.add(*colors++); // r4
    mBatchColors.add(*colors++); // g4
    mBatchColors.add(*colors++); // b4
    mBatchColors.add(*colors);   // a4

    addToBatchSize(4, 2);
}


void GraphicsOpenGL::flush()
{
    if (mCurrentBatchTriangleCount <= 0)
        return;

    switch (mCurrentBatchDrawMode)
    {
    default:
    case IDM_NORMAL:
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        break;
    case IDM_ADD:
        glBlendFunc(GL_SRC_ALPHA, GL_ONE);
        break;
    case IDM_MULTIPLY:
        glBlendFunc(GL_ZERO, GL_SRC_COLOR);
        break;
    case IDM_PREMUL_NORMAL:
        glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
        break;
    case IDM_PREMUL_ADD:
        glBlendFunc(GL_ONE, GL_ONE);
        break;
    }

    // TODO: Add more code to figure out desktop GL version/extensions and enable the active texture calls for it.
    // Currently we're assuming on desktop GL that only single texture 0 is enabled & active!
#ifdef T2_USE_OPENGL_ES
    glActiveTexture(GL_TEXTURE0);
#endif
    glBindTexture(GL_TEXTURE_2D, (GLuint)mCurrentBatchTextureGLId);
    
    glVertexPointer(2, GL_FLOAT, 0, mBatchPositions.getRawData());
#ifdef T2_USE_OPENGL_ES
    glClientActiveTexture(GL_TEXTURE0);
#endif
    glTexCoordPointer(2, GL_FLOAT, 0, mBatchTexCoords.getRawData());
    glColorPointer(4, GL_UNSIGNED_BYTE, 0, mBatchColors.getRawData());

    glDrawElements(GL_TRIANGLES, mCurrentBatchTriangleCount * 3, GL_UNSIGNED_SHORT, mBatchIndices.getRawData());

    ++mStatBatchCount;
    mStatTriangleCount += mCurrentBatchTriangleCount;

    // NB: mCurrentBatchTextureGLId is left as is
    mCurrentBatchVertexCount = 0;
    mCurrentBatchTriangleCount = 0;
    mCurrentBatchDrawMode = IDM_NORMAL;
    mBatchIndices.clear();
    mBatchPositions.clear();
    mBatchTexCoords.clear();
    mBatchColors.clear();
}


} // namespace turska

#endif // T2_USE_OPENGL || T2_USE_OPENGL_ES
